import os
import time
import json
from typing import List, Optional
from datetime import datetime

import requests
from docx import Document
from pydantic import BaseModel as PydanticBaseModel


class BaseModel(PydanticBaseModel):
    class Config:
        extra = 'allow'


class DocxConfig(BaseModel):
    name: str
    student_id: str
    class_name: str


class Config(BaseModel):
    url: str
    cookies_text: str
    contest_id: int
    docx_config: DocxConfig


class Submission(BaseModel):
    create_time: str
    id: str
    language: str
    problem: str
    result: int
    statistic_info: dict
    user_id: int
    username: str

    timestamp: Optional[float]


class SubmissionInfo(Submission):
    code: str


class ContestSubmissions(BaseModel):
    results: List[Submission]
    total: int


class Sample(BaseModel):
    input: str
    output: str


class ProblemInfo(BaseModel):
    description: str
    input_description: str
    output_description: str
    samples: List[Sample]
    source: str
    title: str


class SubmissionProblemInfo(BaseModel):
    submission: SubmissionInfo
    problem: ProblemInfo


class DocxPath:
    def __init__(self, template_path: str, report_path: str, problem_id: str, submission: SubmissionProblemInfo = None):
        self.template_path = template_path
        self.report_path = report_path
        self.problem_id = problem_id
        self.submission = submission


class DocxFormatError(Exception):
    def __init__(self, message: str):
        super(DocxFormatError, self).__init__(message)


class QDUOJDownloader:
    def __init__(self, config_file='config.json'):
        self.config_file = config_file
        with open(self.config_file, 'r', encoding='utf-8') as f:
            self.config = Config(**json.load(f))
        self.cookies_dict = self.text_to_cookies(self.config.cookies_text)

    @staticmethod
    def text_to_cookies(cookies_text: str) -> dict:
        cookies = {}
        cookies_text = cookies_text.replace('Cookie:', '').replace(' ', '')
        for item in cookies_text.split(';'):
            temp = item.split('=')
            cookies[temp[0]] = temp[1]
        return cookies

    def get_contest_submissions(self, limit: int = 100, offset: int = 0) -> ContestSubmissions:
        r = requests.get(self.config.url + '/api/contest_submissions',
                         params={
                             'myself': 1,
                             'contest_id': self.config.contest_id,
                             'limit': limit,
                             'offset': offset
                         },
                         cookies=self.cookies_dict)
        return ContestSubmissions(**r.json()['data'])

    def get_submission(self, id_: str) -> SubmissionInfo:
        r = requests.get(self.config.url + '/api/submission', params={'id': id_}, cookies=self.cookies_dict)
        return SubmissionInfo(**r.json()['data'])

    def get_problem(self, problem_id: str) -> ProblemInfo:
        r = requests.get(self.config.url + '/api/contest/problem',
                         params={
                             'contest_id': self.config.contest_id,
                             'problem_id': problem_id
                         },
                         cookies=self.cookies_dict)
        return ProblemInfo(**r.json()['data'])

    def get_all_contest_submissions(self, ac_only: bool = True, new_only: bool = True) -> List[Submission]:
        limit = 100
        offset = 0
        r = self.get_contest_submissions(limit, offset)
        result = r.results.copy()
        while offset + limit < r.total:
            offset += limit
            result.extend(self.get_contest_submissions(limit, offset).results)

        if ac_only:
            result = list(filter(lambda x: x.result == 0, result))

        if new_only:
            for submission in result:
                submission.timestamp = datetime.fromisoformat(submission.create_time.rstrip('Z')).timestamp()
            result.sort(key=lambda x: x.timestamp)
            new_only_result = list()
            problems = set()
            for submission in result:
                if submission.problem not in problems:
                    problems.add(submission.problem)
                    new_only_result.append(submission)
            result = new_only_result

        return result

    def get_contest_submissions_info(self, contest_submissions: List[Submission]) -> List[SubmissionProblemInfo]:
        return [
            SubmissionProblemInfo(
                submission=self.get_submission(submission.id),
                problem=self.get_problem(submission.problem)
            )
            for submission in contest_submissions
        ]

    def get_docx_paths(self, template_dir: str = 'templates', report_dir: str = 'reports') -> List[DocxPath]:
        docx_paths = []
        for root, dirs, files in os.walk(template_dir):
            for file in files:
                if file.startswith('.'):
                    continue
                docx_path = DocxPath(
                    template_path=os.path.join(root, file),
                    report_path=os.path.join(
                        root.replace(template_dir, report_dir, 1),
                        file.replace('学号', self.config.docx_config.student_id, 1)
                            .replace('姓名', self.config.docx_config.name, 1)
                    ),
                    problem_id=file.strip('.docx').split('题号')[-1]
                )
                docx_paths.append(docx_path)
        return docx_paths

    def modify_docx(self, docx_path: str, code_text: str):
        doc = Document(docx_path)

        target_cells = {}
        for row in doc.tables[0].rows:
            for index, cell in enumerate(row.cells):
                if cell.text == '班\t级':
                    target_cells['class_name'] = row.cells[index + 1]
                elif cell.text == '姓\t名':
                    target_cells['name'] = row.cells[index + 1]
                elif cell.text == '学\t号':
                    target_cells['student_id'] = row.cells[index + 1]
        for target_cell_key in ('class_name', 'name', 'student_id'):
            if target_cell_key not in target_cells.keys():
                raise DocxFormatError(f'无法找到"{target_cell_key}"单元格')
            if target_cells[target_cell_key].text != '':
                raise DocxFormatError(f'"{target_cell_key}"单元格的下一个单元格非空')

        code_paragraph = None
        for index, paragraph in enumerate(doc.paragraphs):
            if paragraph.text == '源代码：':
                code_paragraph = doc.paragraphs[index + 1]
                break
        if code_paragraph is None:
            raise DocxFormatError('无法找到"源代码："段落')
        if code_paragraph.text != '':
            raise DocxFormatError('"源代码："段落的下一个段落非空')

        target_cells['class_name'].text = self.config.docx_config.class_name
        target_cells['name'].text = self.config.docx_config.name
        target_cells['student_id'].text = self.config.docx_config.student_id
        code_paragraph.text = code_text

        return doc

    def run(self):
        print('Start: 获取提交记录')
        ac_submissions_info = self.get_contest_submissions_info(
            self.get_all_contest_submissions(ac_only=True, new_only=True)
        )
        print('Done: 获取提交记录')

        problem_id_to_submission = {
            submission.submission.problem: submission
            for submission in ac_submissions_info
        }

        with open('submissions.json', 'w', encoding='utf-8') as f:
            json.dump(list(map(lambda x: x.dict(), ac_submissions_info)), f, ensure_ascii=False, sort_keys=True)

        docx_paths = self.get_docx_paths()
        for docx_path in docx_paths:
            docx_path.problem_id = '9' + docx_path.problem_id[1:]
            if docx_path.problem_id in problem_id_to_submission.keys():
                docx_path.submission = problem_id_to_submission[docx_path.problem_id]
            else:
                print(f'尚未完成的题目: {docx_path.problem_id}')

        print('Start: 写入实验报告')
        for docx_path in list(filter(lambda x: x.submission is not None, docx_paths)):
            doc = self.modify_docx(docx_path=docx_path.template_path, code_text=docx_path.submission.submission.code)
            os.makedirs(os.path.dirname(docx_path.report_path), exist_ok=True)
            doc.save(docx_path.report_path)

        print('Done!')


if __name__ == '__main__':
    if not os.path.isfile('config.json'):
        with open('config.json', 'w', encoding='utf-8') as fp:
            json.dump({
                'url': 'https://qduoj.com',
                'cookies_text': 'sessionid=xxxxxx',
                'contest_id': '42',
                'docx_config': {
                    'name': '张三',
                    'student_id': "12345678900",
                    "class_name": "20计算机1班"
                }
            }, fp, ensure_ascii=False, indent=4)
        print('配置文件 config.json 不存在，已自动创建，请修改配置文件后重新运行')
        time.sleep(3)
        exit()
    QDUOJDownloader().run()
